/*! \file
 * \brief Enter a brief description here.
 *
 * Enter a detailed description here. (optional)
 * \author Author (Ivo Wingelaar)
 * \date Date (2011)
 * \version Version (0.0.1)
 * \copyright Copyright (GNU LGPL v3)
 */

#include "LynxGLSLMaterialRenderer.hpp"

#include "LynxMaterialRendererServices.hpp"
#include "LynxVideoManager_C.hpp"

namespace Lynx
{

//! Constructor
GLSLMaterialRenderer::GLSLMaterialRenderer(VideoManager_C* driver,
        int32_t& outMaterialTypeNr, const char* vertexShaderProgram,
        const char* vertexShaderEntryPointName,
        E_VERTEX_SHADER_TYPE vsCompileTarget,
        const char* pixelShaderProgram,
        const char* pixelShaderEntryPointName,
        E_PIXEL_SHADER_TYPE psCompileTarget,
        const char* geometryShaderProgram,
        const char* geometryShaderEntryPointName,
        E_GEOMETRY_SHADER_TYPE gsCompileTarget,
        E_PRIMITIVE_TYPE inType, E_PRIMITIVE_TYPE outType,
        uint32_t verticesOut,
        ShaderConstantSetCallBack* callback,
        MaterialRenderer* baseMaterial,
        int32_t userData) :
    mDriver(driver),
    mCallBack(callback),
    mBaseMaterial(baseMaterial),
    mProgram(0),
    mUserData(userData)
{
    //entry points must always be main, and the compile target isn't selectable
    //it is fine to ignore what has been asked for, as the compiler should spot anything wrong
    //just check that GLSL is available

    if(mBaseMaterial)
        mBaseMaterial->increment_counter();

    if(mCallBack)
        mCallBack->increment_counter();

    if(!mDriver->query_feature(EVMF_ARB_GLSL))
        return;

    init(outMaterialTypeNr, vertexShaderProgram, pixelShaderProgram, geometryShaderProgram);
}


//! constructor only for use by derived classes who want to
//! create a fall back material for example.
GLSLMaterialRenderer::GLSLMaterialRenderer(VideoManager_C* driver,
        ShaderConstantSetCallBack* callback,
        MaterialRenderer* baseMaterial, int32_t userData)
    :
    mDriver(driver),
    mCallBack(callback),
    mBaseMaterial(baseMaterial),
    mProgram(0),
    mUserData(userData)
{
    if(mBaseMaterial)
        mBaseMaterial->increment_counter();

    if(mCallBack)
        mCallBack->increment_counter();
}


//! Destructor
GLSLMaterialRenderer::~GLSLMaterialRenderer()
{
    if(mCallBack)
        mCallBack->decrement_counter();

    if(mProgram)
    {
        GLhandleARB shaders[8];
        GLint count;
        mDriver->extGlGetAttachedObjects(mProgram, 8, &count, shaders);
        for(GLint i=0; i<count; ++i)
            mDriver->extGlDeleteObject(shaders[i]);
        mDriver->extGlDeleteObject(mProgram);
        mProgram = 0;
    }

    mUniformInfo.clear();

    if(mBaseMaterial)
        mBaseMaterial->decrement_counter();
}


void GLSLMaterialRenderer::init(int32_t& outMaterialTypeNr,
                                const char* vertexShaderProgram,
                                const char* pixelShaderProgram,
                                const char* geometryShaderProgram,
                                E_PRIMITIVE_TYPE inType, E_PRIMITIVE_TYPE outType,
                                uint32_t verticesOut)
{
    outMaterialTypeNr = -1;

    if(!createProgram())
        return;

#if defined(GL_ARB_vertex_shader) && defined (GL_ARB_fragment_shader)
    if(vertexShaderProgram)
        if(!createShader(GL_VERTEX_SHADER_ARB, vertexShaderProgram))
            return;

    if(pixelShaderProgram)
        if(!createShader(GL_FRAGMENT_SHADER_ARB, pixelShaderProgram))
            return;
#endif

#if defined(GL_ARB_geometry_shader4) || defined(GL_EXT_geometry_shader4) || defined(GL_NV_geometry_program4) || defined(GL_NV_geometry_shader4)
    if(geometryShaderProgram && mDriver->query_feature(EVMF_GEOMETRY_SHADER))
    {
        if(!createShader(GL_GEOMETRY_SHADER_EXT, geometryShaderProgram))
            return;
#if defined(GL_ARB_geometry_shader4) || defined(GL_EXT_geometry_shader4) || defined(GL_NV_geometry_shader4)
        mDriver->extGlProgramParameteri((GLuint)mProgram, GL_GEOMETRY_INPUT_TYPE_EXT, mDriver->primitive_type_to_gl(inType));
        mDriver->extGlProgramParameteri((GLuint)mProgram, GL_GEOMETRY_OUTPUT_TYPE_EXT, mDriver->primitive_type_to_gl(outType));
        if(verticesOut==0)
            mDriver->extGlProgramParameteri((GLuint)mProgram, GL_GEOMETRY_VERTICES_OUT_EXT, mDriver->mMaxGeometryVerticesOut);
        else
            mDriver->extGlProgramParameteri((GLuint)mProgram, GL_GEOMETRY_VERTICES_OUT_EXT, Myth::Math::min(verticesOut, mDriver->mMaxGeometryVerticesOut));
#elif defined(GL_NV_geometry_program4)
        if(verticesOut==0)
            mDriver->extGlProgramVertexLimit(GL_GEOMETRY_PROGRAM_NV, mDriver->mMaxGeometryVerticesOut);
        else
            mDriver->extGlProgramVertexLimit(GL_GEOMETRY_PROGRAM_NV, Myth::Math::min(verticesOut, mDriver->mMaxGeometryVerticesOut));
#endif
    }
#endif

    if(!linkProgram())
        return;

    // register myself as new material
    outMaterialTypeNr = mDriver->add_material_renderer(this);
}


bool GLSLMaterialRenderer::on_render(MaterialRendererServices* service,
                                    E_VERTEX_TYPE vtxtype)
{
    // call callback to set shader constants
    if(mCallBack && mProgram)
        mCallBack->on_set_constants(this, mUserData);

    return true;
}


void GLSLMaterialRenderer::on_set_material(const Material& material,
        const Material& lastMaterial,
        bool resetAllRenderstates,
        MaterialRendererServices* services)
{
    if(material.mMaterialType != lastMaterial.mMaterialType || resetAllRenderstates)
    {
        if(mProgram)
            mDriver->extGlUseProgramObject(mProgram);

        if(mBaseMaterial)
            mBaseMaterial->on_set_material(material, material, true, this);
    }

    //let callback know used material
    if(mCallBack)
        mCallBack->on_set_material(material);

    for(uint32_t i=0; i<8; ++i)
        mDriver->set_active_texture(i, material.get_texture(i));
    mDriver->set_basic_render_states(material, lastMaterial, resetAllRenderstates);
}


void GLSLMaterialRenderer::on_unset_material()
{
    mDriver->extGlUseProgramObject(0);

    if(mBaseMaterial)
        mBaseMaterial->on_unset_material();
}


//! Returns if the material is transparent.
bool GLSLMaterialRenderer::is_transparent() const
{
    return mBaseMaterial ? mBaseMaterial->is_transparent() : false;
}


bool GLSLMaterialRenderer::createProgram()
{
    mProgram = mDriver->extGlCreateProgramObject();
    return true;
}


bool GLSLMaterialRenderer::createShader(GLenum shaderType, const char* shader)
{
    GLhandleARB shaderHandle = mDriver->extGlCreateShaderObject(shaderType);

    mDriver->extGlShaderSourceARB(shaderHandle, 1, &shader, NULL);
    mDriver->extGlCompileShaderARB(shaderHandle);

    GLint status = 0;

#ifdef GL_ARB_shader_objects
    mDriver->extGlGetObjectParameteriv(shaderHandle, GL_OBJECT_COMPILE_STATUS_ARB, &status);
#endif

    if(!status)
    {
        mDriver->get_log_manager()->log("GLSL shader failed to compile", ELL_ERROR);
        // check error message and log it
        GLint maxLength=0;
        GLsizei length;
#ifdef GL_ARB_shader_objects
        mDriver->extGlGetObjectParameteriv(shaderHandle,
                                           GL_OBJECT_INFO_LOG_LENGTH_ARB, &maxLength);
#endif
        GLcharARB* infoLog = new GLcharARB[maxLength];
        mDriver->extGlGetInfoLog(shaderHandle, maxLength, &length, infoLog);
        mDriver->get_log_manager()->log(reinterpret_cast<const char*>(infoLog), ELL_ERROR);
        delete [] infoLog;

        return false;
    }

    mDriver->extGlAttachObject(mProgram, shaderHandle);

    return true;
}


bool GLSLMaterialRenderer::linkProgram()
{
    mDriver->extGlLinkProgramARB(mProgram);

    GLint status = 0;

#ifdef GL_ARB_shader_objects
    mDriver->extGlGetObjectParameteriv(mProgram, GL_OBJECT_LINK_STATUS_ARB, &status);
#endif

    if(!status)
    {
        mDriver->get_log_manager()->log("GLSL shader program failed to link", ELL_ERROR);
        // check error message and log it
        GLint maxLength=0;
        GLsizei length;
#ifdef GL_ARB_shader_objects
        mDriver->extGlGetObjectParameteriv(mProgram,
                                           GL_OBJECT_INFO_LOG_LENGTH_ARB, &maxLength);
#endif
        GLcharARB* infoLog = new GLcharARB[maxLength];
        mDriver->extGlGetInfoLog(mProgram, maxLength, &length, infoLog);
        mDriver->get_log_manager()->log(reinterpret_cast<const char*>(infoLog), ELL_ERROR);
        delete [] infoLog;

        return false;
    }

    // get uniforms information

    GLint num = 0;
#ifdef GL_ARB_shader_objects
    mDriver->extGlGetObjectParameteriv(mProgram, GL_OBJECT_ACTIVE_UNIFORMS_ARB, &num);
#endif

    if(num == 0)
    {
        // no uniforms
        return true;
    }

    GLint maxlen = 0;
#ifdef GL_ARB_shader_objects
    mDriver->extGlGetObjectParameteriv(mProgram, GL_OBJECT_ACTIVE_UNIFORM_MAX_LENGTH_ARB, &maxlen);
#endif

    if(maxlen == 0)
    {
        mDriver->get_log_manager()->log("GLSL: failed to retrieve uniform information", ELL_ERROR);
        return false;
    }

    // seems that some implementations use an extra null terminator
    ++maxlen;
    char* buf = new char[maxlen];

    mUniformInfo.clear();
    mUniformInfo.reserve(num);

    for(int i=0; i < num; ++i)
    {
        UniformInfo ui;
        memset(buf, 0, maxlen);

        GLint size;
        mDriver->extGlGetActiveUniformARB(mProgram, i, maxlen, 0, &size, &ui.type, reinterpret_cast<GLcharARB*>(buf));
        ui.name = buf;

        mUniformInfo.push_back(ui);
    }

    delete [] buf;

    return true;
}


void GLSLMaterialRenderer::set_basic_render_states(const Material& material,
        const Material& lastMaterial,
        bool resetAllRenderstates)
{
    // forward
    mDriver->set_basic_render_states(material, lastMaterial, resetAllRenderstates);
}


bool GLSLMaterialRenderer::set_vertex_shader_constant(const char* name, const float* floats, int count)
{
    return set_pixel_shader_constant(name, floats, count);
}


void GLSLMaterialRenderer::set_vertex_shader_constant(const float* data, int32_t startRegister, int32_t constantAmount)
{
    mDriver->get_log_manager()->log("Cannot set constant, please use high level shader call instead.", ELL_WARNING);
}

bool GLSLMaterialRenderer::set_pixel_shader_constant(const char* name, const float* floats, int count)
{
    uint32_t i;
    const uint32_t num = mUniformInfo.size();

    for(i=0; i < num; ++i)
    {
        if(mUniformInfo[i].name == name)
            break;
    }

    if(i == num)
        return false;

#ifdef GL_ARB_shader_objects
    GLint Location=mDriver->extGlGetUniformLocationARB(mProgram,name);

    switch(mUniformInfo[i].type)
    {
    case GL_FLOAT:
        mDriver->extGlUniform1fv(Location, count, floats);
        break;
    case GL_FLOAT_VEC2_ARB:
        mDriver->extGlUniform2fv(Location, count/2, floats);
        break;
    case GL_FLOAT_VEC3_ARB:
        mDriver->extGlUniform3fv(Location, count/3, floats);
        break;
    case GL_FLOAT_VEC4_ARB:
        mDriver->extGlUniform4fv(Location, count/4, floats);
        break;
    case GL_FLOAT_MAT2_ARB:
        mDriver->extGlUniformMatrix2fv(Location, count/4, false, floats);
        break;
    case GL_FLOAT_MAT3_ARB:
        mDriver->extGlUniformMatrix3fv(Location, count/9, false, floats);
        break;
    case GL_FLOAT_MAT4_ARB:
        mDriver->extGlUniformMatrix4fv(Location, count/16, false, floats);
        break;
    default:
        mDriver->extGlUniform1iv(Location, count, reinterpret_cast<const GLint*>(floats));
        break;
    }
    return true;
#else
    return false;
#endif

}

void GLSLMaterialRenderer::set_pixel_shader_constant(const float* data, int32_t startRegister, int32_t constantAmount)
{
    mDriver->get_log_manager()->log("Cannot set constant, use high level shader call.", ELL_WARNING);
}

VideoManager* GLSLMaterialRenderer::get_video_manager()
{
    return mDriver;
}

} // namespace Lynx
